#!/usr/bin/env python3

__authors__ = [ 'zPlus <zplus@peers.community>' ]
__version__ = '0.0.0'
__license__ = 'AGPL-3.0-or-later'
###############################################################################

import bottle, json, requests
import os
import time
import subprocess
import gitosis_configen
from bottle import get, HTTPResponse, post, request, route

# Load settings
import settings

# Fire up database
import database

PROTOCOL = "http://"

def debug_log(message):
    with open(os.path.join("..",settings.log_path), mode = "a") as file:
        file.write(str(time.thread_time()) + " " + message +  "\n")
        file.close()


    

# This is used to export the bottle object for the WSGI server
application = bottle.app()

if not "gitosis-admin" in os.listdir():
    subprocess.run(["git","clone", "ssh://gitosis@127.0.0.1/gitosis-admin"])
    if not "gitosis-admin" in os.listdir():
        debug_log("couldn't clone gitosis admin")
        exit(1)

os.chdir("gitosis-admin")

def commit_all(message):
    subprocess.run(["git","add","-A"])
    subprocess.run(["git","commit","-a","-m", '"' + message + '"'])
    subprocess.run(["git","push"])

    

@get('/inbox/<forge>/<actor>')
def read_inbox(forge, actor):
    ''' Read INBOX. '''
    
    if forge not in settings.forge:
        return HTTPResponse(status=400, body=None)
    
    if 'Authorization' in request.headers:
        ''' Forge own INBOX. '''
        
        if request.get_header('Authorization') != 'Bearer {}'.format(settings.forge[forge]['authorization_token']):
            return HTTPResponse(status=401, body=None)
        
        messages = database.get_inbox_messages(forge, actor)
        
        return {
            "@context": "https://www.w3.org/ns/activitystreams",
            "summary": actor + " messages",
            "type": "OrderedCollection",
            "totalItems": len(messages),
            "orderedItems": messages
        }
    
    else:
        ''' Public access to INBOX. '''
        
        pass # Not implemented

@post('/inbox/<forge>/<actor>')
def write_inbox(forge, actor):
    ''' Write to INBOX. '''
    
    if forge not in settings.forge:
        return HTTPResponse(status=400, body=None)
    
    if 'Authorization' in request.headers:
        if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
            return HTTPResponse(status=401, body=None)
        
        pass # Not implemented
    
    else:
        ''' We have received a messaged from a remote actor to our INBOX. '''
        
        message = request.json
        
        if not message:
            return HTTPResponse(status=400, body=None)
        
        # If the action is "Follow", remember remote followers
        if message['type'] == 'Follow':
            database.add_follower(forge, message['actor'], actor)
        
        # Store message
        database.store_inbox(forge, actor, message)
        
        # Forward message to the forge
        if settings.forge[forge]['callback'] is not None:
            requests.post(
                settings.forge[forge]['callback'] + actor,
                headers = {
                    'Authorization': 'Bearer {}'.format(settings.forge[forge]['authorization_token'])
                },
                json = message)

@get('/outbox/<forge>/<actor>')
def read_outbox(forge, actor):
    ''' Read OUTBOX. '''
    
    if forge not in settings.forge:
        return HTTPResponse(status=400, body=None)
    
    if 'Authorization' in request.headers:
        ''' Forge own OUTBOX. '''
        
        pass # Not implemented
    else:
        pass # Not implemented

@post('/outbox/<forge>/<actor>')
def write_outbox(forge, actor):
    ''' Write to OUTBOX. '''
    
    if forge not in settings.forge:
        return HTTPResponse(status=400, body=None)
    
    # Must be authenticated to write the OUTBOX
    if 'Authorization' not in request.headers:
        return HTTPResponse(status=401, body=None)
    
    # Validate authentication token
    if request.get_header('Authorization') != 'Bearer ' + settings.forge[forge]['authorization_token']:
        return HTTPResponse(status=401, body=None)
    
    message = request.json
    
    # Store message to database
    database.store_outbox(forge, actor, message)
    
    if message['type'] == 'Follow':
        # Retrieve remote actor
        response = requests.get(message['object'], headers={ 'Content-Type': 'application/json' })
        
        if response.status_code == 200:
            remote_actor = response.json()
            
            database.add_following(forge, actor, message['object'])
            
            # Now send a notification to the actor being followed
            requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)
    elif message['type'] == 'Create':
        # If there is no "to:" field, assume "all followers"
        if "to" not in message:
            message['to'] = database.get_followers(forge, actor)
        
        if not isinstance(message['to'], list):
            message['to'] = [ message['to'] ]
        
        for follower in message['to']:
            # Retrieve remote actor
            response = requests.get(follower, headers={ 'Content-Type': 'application/json' })
            
            if response.status_code == 200:
                remote_actor = response.json()
                
                # Now send a notification to the actor being followed
                requests.post(remote_actor['inbox'], headers={ 'Content-Type': 'application/json' }, json=message)


def key_path(name):
    
    key_name = "{}.pub".format(name)
    key_path = os.path.join("keydir", key_name)
    return key_path

def add_key(name,key):
    
    with open(key_path(name), mode="w") as file:
        file.write(key)
        file.close()

def key_exists(name):
    if os.path.isfile(key_path(name)):
        return True
    return False

def import_foreign_key(address):
    
    user, host = address.split("@")
    foreigner = PROTOCOL+host+"/users/"+user #IMPORTANT: don't forget to change if routing changes
    response = requests.get(foreigner, headers={ 'Content-Type': 'application/json' })
    if response.status_code != 200:
        return False
    res_body = response.json()
    if "publicKey" not in res_body.keys():
        return False    
    
    add_key(address, res_body["publicKey"]) # we add a key without adding a user to a db
    
    return True

@post('/users/register')
def user_register():
    ''' Register a user '''
    #TODO: it shouldn't contain @ or be "admin-daemon" or "register" obviously
    
    debug_log("incoming post at /users/register")
    
    required_keys = [
                "username",
                "password",
                "email",
                "key", 
    ]
    
    req = request.json

    if any([(x not in req.keys()) for x in required_keys]):
        return HTTPResponse(status=400, body=None)

    if key_exists(req["username"]):
        return HTTPResponse(status=400, body=None)
    #TODO also it should respond with 400 if smb tries to add the same key under a different username

    res = database.add_user(
           req["username"],
           req["email"],
           req["password"],
           req["key"],
    )
    
    if not res:
        return HTTPResponse(status=400, body=None)
    
    add_key(req["username"], req["key"])
    
    commit_all("daemon: user created")
    
    
    return HTTPResponse(status=204, body=None)

@post('/users/<user>/new')
def add_repo(user):
    ''' make a new repo '''
    
    debug_log("incoming post at /users/" + user + "/new")
    
    required_keys = [
                "repository_name",
    ]
    
    req = request.json

    if any([(x not in req.keys()) for x in required_keys]):
        return HTTPResponse(status=400, body=None)

    if not key_exists(user): #check user exists
        return HTTPResponse(status=400, body=None)
    
    if req["repository_name"] in [ x["repository"] for x in database.get_repositories() ]: #check repo nonexistent
        return HTTPResponse(status=400, body=None)
    
    database.add_repository( #TODO maybe better to move existence check to add function
           req["repository_name"],
           user,
    )
    
    database.regenerate_gitosis_config()
        
    commit_all("daemon: repo created")
    return HTTPResponse(status=204, body=None)

@post('/users/<user>/<repository_name>/collaborators/new')
def add_collaboration(user, repository_name):
    ''' make a new collaboration'''
    
    debug_log("incoming post at /users/" + user + "/" + repository_name + "/collaborators/new")
    
    required_keys = [ # complex collaborators names including instances are bad for inlining in address
                "collaborator",
    ]
    
    req = request.json

    if any([(x not in req.keys()) for x in required_keys]):
        return HTTPResponse(status=400, body="some fields are missing")

    if not key_exists(user): #check user exists
        return HTTPResponse(status=404, body=None)
    
    if not repository_name in [ x["repository"] for x in database.get_repositories() ]:#check repo
        return HTTPResponse(status=404, body=None)
    
    if req["collaborator"] in database.get_collaborators(repository_name):
        return HTTPResponse(status=400, body="collaborator exists")
    
    if "@" in req["collaborator"]:
        if not import_foreign_key(req["collaborator"]):
            return HTTPResponse(status=400, body="foreign collaborator does not exist")
        
    else:
        #TODO:check existence of local collaborator's user
        pass
    
    database.add_collaboration(
           repository_name, #TODO: disambugating reponames so different users can have repos with same names 
           req["collaborator"],
    )
    
    database.regenerate_gitosis_config()
        
    commit_all("daemon: collaborator added")
    return HTTPResponse(status=204, body=None)

#@get('/<forge>/<actor>')
#def describe_actor(forge, actor):
#    ''' Return profile of an actor. '''
#    
#    if forge not in settings.forge:
#        return HTTPResponse(status=400, body=None)
#    
#    return {
#        '@context':  'https://www.w3.org/ns/activitystreams',
#        'type':      '',
#        'id':        '{}/{}/{}'.format(settings.mcfi_url, forge, actor),
#        'name':      '{}'.format(actor),
#        'summary':   '',
#        'inbox':     '{}/inbox/{}/{}'.format(settings.mcfi_url, forge, actor),
#        'outbox':    '{}/outbox/{}/{}'.format(settings.mcfi_url, forge, actor),
#        'followers': '{}/followers/{}/{}'.format(settings.mcfi_url, forge, actor),
#        'following': '{}/following/{}/{}'.format(settings.mcfi_url, forge, actor)
#    }

@get('/users/<username>')
def describe_user(username):
    ''' Return profile of a user '''

    id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
    user = database.get_user_local(username)

    if not user:
        return HTTPResponse(status=400, body=None)
    
    return {
            '@context':  [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.peers.community/ns"
            ],
            'type':      'Person',
            'id':        id_,
            'name':      '{}'.format(username),
            'preferredUsername': '{}'.format(username),
            'summary':   '',
            'inbox':     '{}/inbox'.format(id_),
            'outbox':    '{}/outbox'.format(id_),
            'followers': '{}/followers'.format(id_),
            'following': '{}/following'.format(id_),
            'publicKey': user["public_key"]
        }

@get('/users/<username>/<repository>')
def describe_repo(username,repository):
    ''' describe a repo'''

    #id_ = '{}/{}'.format(settings.mcfi_url, 'users',username )
    #user = database.get_user_local(username)

#    if not user:
#        return HTTPResponse(status=400, body=None)
    
    return {
            '@context':  [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.peers.community/ns"
            ],
            'collaborators':  json.dumps(database.get_collaborators(repository))#TODO:don't forget if reponame mech changes.
#            'id':        id_,
#            'name':      '{}'.format(username),
#            'preferredUsername': '{}'.format(username),
#            'summary':   '',
#            'inbox':     '{}/inbox'.format(id_),
#            'outbox':    '{}/outbox'.format(id_),
#            'followers': '{}/followers'.format(id_),
#            'following': '{}/following'.format(id_),
#            'publicKey': user["public_key"]
        }


@get('/administration/reset-database')
def reset_db():
    ''' drop all users and reset things'''
    if settings.DEBUG:
        for key in os.listdir("keydir"):
            if key != 'admin-daemon.pub':
                path = os.path.join("keydir", key)
                os.remove(path)
        
        config = gitosis_configen.Config()
        config.flush()
        commit_all("daemon: drop all users")
        db_path=os.path.join("..",settings.database_path)
        if os.path.isfile(db_path):
            os.remove(db_path)
        exit(0) #forcing a worker restart so that restarting worker regenerates database.

@get('/oauth-clients/local')
def generate_clientid():
    ''' Generate or "generate" client id and secret''' #maybe real generation someday, but i can't see a point yet
    
    return {
        'client_id': settings.client_id,
        'client_secret': settings.client_secret,
    }


@route('/')
def index():
    return 'MCFI version ' + __version__






